/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { HttpUrl } from "./HttpUrl"
import { kotlinRemovePrefix, kotlinRequire } from './utils/Utils'
import { canParseAsIpAddress, delimiterOffset,
  indexOfControlOrNonAscii,
  toCanonicalHost, trimSubstring } from "./utils/StringUtil"
import { List } from "@kit.ArkTS"
import { Headers } from "./Headers"
import { IllegalArgumentException } from "./Error/error"

// December 31, 9999 23:59:59.999 GMT (same as OkHttp's MAX_DATE)
const MAX_DATE = 253402300799999;

/**
 * An [RFC 6265](http://tools.ietf.org/html/rfc6265) Cookie.
 *
 * This class doesn't support additional attributes on cookies, like
 * [Chromium's Priority=HIGH extension][chromium_extension].
 *
 * [chromium_extension]: https://code.google.com/p/chromium/issues/detail?id=232693
 */
// @Suppress("NAME_SHADOWING")
export class Cookie {
  /** Returns a non-empty string with this cookie's name. */
  // @get:JvmName("name") val name: String,
  #name: string
  get name(): string{
    return this.#name
  }
  /** Returns a possibly-empty string with this cookie's value. */
  // @get:JvmName("value") val value: String,
  #value: string
  get value(): string{
    return this.#value
  }
  /**
   * Returns the time that this cookie expires, in the same format as [System.currentTimeMillis].
   * This is December 31, 9999 if the cookie is [persistent], in which case it will expire at the
   * end of the current session.
   *
   * This may return a value less than the current time, in which case the cookie is already
   * expired. Webservers may return expired cookies as a mechanism to delete previously set cookies
   * that may or may not themselves be expired.
   */
  // @get:JvmName("expiresAt") val expiresAt: Long,
  #expiresAt: number
  get expiresAt(): number{
    return this.#expiresAt
  }

  /**
   * Returns the cookie's domain. If [hostOnly] returns true this is the only domain that matches
   * this cookie; otherwise it matches this domain and all subdomains.
   */
  // @get:JvmName("domain") val domain: String,
  #domain: string
  get domain(): string{
    return this.#domain
  }

  /**
   * Returns this cookie's path. This cookie matches URLs prefixed with path segments that match
   * this path's segments. For example, if this path is `/foo` this cookie matches requests to
   * `/foo` and `/foo/bar`, but not `/` or `/football`.
   */
  // @get:JvmName("path") val path: String,
  #path: string
  get path(): string{
    return this.#path
  }

  /** Returns true if this cookie should be limited to only HTTPS requests. */
  // @get:JvmName("secure") val secure: Boolean,
  #secure: boolean
  get secure(): boolean{
    return this.#secure
  }

  /**
   * Returns true if this cookie should be limited to only HTTP APIs. In web browsers this prevents
   * the cookie from being accessible to scripts.
   */
  // @get:JvmName("httpOnly") val httpOnly: Boolean,
  #httpOnly: boolean
  get httpOnly(): boolean{
    return this.#httpOnly
  }

  /** Returns true if this cookie does not expire at the end of the current session. */
  // @get:JvmName("persistent") val persistent: Boolean, // True if 'expires' or 'max-age' is present.
  #persistent: boolean
  get persistent(): boolean{
    return this.#persistent
  }

  /**
   * Returns true if this cookie's domain should be interpreted as a single host name, or false if
   * it should be interpreted as a pattern. This flag will be false if its `Set-Cookie` header
   * included a `domain` attribute.
   *
   * For example, suppose the cookie's domain is `example.com`. If this flag is true it matches
   * **only** `example.com`. If this flag is false it matches `example.com` and all subdomains
   * including `api.example.com`, `www.example.com`, and `beta.api.example.com`.
   */
  // @get:JvmName("hostOnly") val hostOnly: Boolean // True unless 'domain' is present.
  #hostOnly: boolean
  get hostOnly(): boolean{
    return this.#hostOnly
  }

  private constructor(name: string, value: string, expiresAt: number, domain: string, path: string, secure: boolean, httpOnly: boolean, persistent: boolean, hostOnly: boolean){
    this.#name = name
    this.#value = value
    this.#expiresAt = expiresAt
    this.#domain = domain
    this.#path = path
    this.#secure = secure
    this.#httpOnly = httpOnly
    this.#persistent = persistent
    this.#hostOnly = hostOnly
  }
  /**
   * Returns true if this cookie should be included on a request to [url]. In addition to this
   * check callers should also confirm that this cookie has not expired.
   */
  matches(url: HttpUrl): boolean {
    let domainMatch: boolean
    if (this.#hostOnly){
      domainMatch = url.host == this.#domain
    }else{
      domainMatch = Cookie.domainMatch(url.host, this.#domain)
    }
    if (!domainMatch) return false

    if (!Cookie.pathMatch(url, this.#path)) return false

    return !this.#secure || url.isHttps
  }

  equals(other: unknown): boolean {
    return other instanceof  Cookie &&
    other.name == this.#name &&
    other.value == this.#value &&
    other.expiresAt == this.#expiresAt &&
    other.domain == this.#domain &&
    other.path == this.#path &&
    other.secure == this.#secure &&
    other.httpOnly == this.#httpOnly &&
    other.persistent == this.#persistent &&
    other.hostOnly == this.#hostOnly
  }

  // @IgnoreJRERequirement // As of AGP 3.4.1, D8 desugars API 24 hashCode methods.
  hashCode(): number {
    let result = 17
    result = 31 * result + this.#hashString(this.#name)
    result = 31 * result + this.#hashString(this.#value)
    result = 31 * result + this.#hashNumber(this.#expiresAt)
    result = 31 * result + this.#hashString(this.#domain)
    result = 31 * result + this.#hashString(this.#path)
    result = 31 * result + (this.#secure ? 1 : 0)
    result = 31 * result + (this.#httpOnly ? 1 : 0)
    result = 31 * result + (this.#persistent ? 1 : 0)
    result = 31 * result + (this.#hostOnly ? 1 : 0)
    return result
  }

  #hashString(s: string): number {
    let hash = 0
    for (let i = 0; i < s.length; i++) {
      hash = ((hash << 5) - hash) + s.charCodeAt(i)
      hash = hash & hash // Convert to 32bit integer
    }
    return hash
  }

  #hashNumber(n: number): number {
    return n ^ (n >>> 32) // XOR with high bits for better distribution
  }

  toString(): string {
    return this.#toString(false)
  }

  /**
   * @param forObsoleteRfc2965 true to include a leading `.` on the domain pattern. This is
   *     necessary for `example.com` to match `www.example.com` under RFC 2965. This extra dot is
   *     ignored by more recent specifications.
   */
  #toString(forObsoleteRfc2965: boolean): string {
    let result: string = ""
    result += this.#name + "=" + this.#value
    if (this.#persistent) {
      if (this.#expiresAt == Number.MIN_VALUE) {
        result += "; max-age=0"
      } else {
        result += "; expires=" + new Date(this.#expiresAt.toString()).toDateString() // toHttpDateString()
      }
    }
    if (!this.#hostOnly) {
      result += "; domain="
      if (forObsoleteRfc2965) {
        result += "."
      }
      result += this.#domain
    }
    result += "; path=" + this.#path
    if (this.#secure) {
      result += "; secure"
    }
    if (this.#httpOnly) {
      result += "; httponly"
    }
    return result
  }
  /**
   * Builds a cookie. The [name], [value], and [domain] values must all be set before calling
   * [build].
   */
  static Builder = class {
    #name?: string = undefined
    #value?: string = undefined
    #expiresAt = MAX_DATE
    #domain?: string = undefined
    #path = "/"
    #secure = false
    #httpOnly = false
    #persistent = false
    #hostOnly = false

    name(name: string){
      kotlinRequire(name.trim() === name, "name is not trimmed")
      this.#name = name
      return this
    }

    value(value: string){
      kotlinRequire(value.trim() === value, "value is not trimmed")
      this.#value = value
      return this
    }

    expiresAt(expiresAt: number){
      let _expiresAt = expiresAt
      if (_expiresAt <= 0) _expiresAt = Number.MIN_VALUE
      if (_expiresAt > MAX_DATE) _expiresAt = MAX_DATE
      this.#expiresAt = _expiresAt
      this.#persistent = true
      return this
    }
    /**
     * Set the domain pattern for this cookie. The cookie will match [domain] and all of its
     * subdomains.
     */
    domain(domain: string){
      return this.#domain_(domain, false)
    }

    /**
     * Set the host-only domain for this cookie. The cookie will match [domain] but none of
     * its subdomains.
     */
    hostOnlyDomain(domain: string){
      return this.#domain_(domain, true)
    }

    #domain_(domain: string, hostOnly: boolean){
      let canonicalDomain: string | null
      canonicalDomain = toCanonicalHost(domain)
      if (canonicalDomain === null) {
        throw new IllegalArgumentException(`unexpected domain: ${domain}`)
      }
      this.#domain = canonicalDomain
      this.#hostOnly = hostOnly
      return this
    }

    path(path: string){
      kotlinRequire(path.startsWith("/"),'path must start with /')
      this.#path = path
      return this
    }

    secure(){
      this.#secure = true
      return this
    }

    httpOnly(){
      this.#httpOnly = true
      return this
    }

    build(): Cookie {
      if (this.#name === undefined) throw new IllegalArgumentException("builder.name === undefined")
      if (this.#value === undefined) throw new IllegalArgumentException("builder.value === undefined")
      if (this.#domain === undefined) throw new IllegalArgumentException("builder.domain === undefined")
      return new Cookie(
        this.#name,
      this.#value,
      this.#expiresAt,
      this.#domain,
      this.#path,
      this.#secure,
      this.#httpOnly,
      this.#persistent,
      this.#hostOnly)
    }
  }

  private static YEAR_PATTERN = /(\d{2,4})[^\d]*/
  private static MONTH_PATTERN = /(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec).*/i
  private static DAY_OF_MONTH_PATTERN = /(\d{1,2})[^\d]*/
  private static TIME_PATTERN = /(\d{1,2}):(\d{1,2}):(\d{1,2})[^\d]*/
  private static domainMatch(urlHost: string, domain: string): boolean {
    if (urlHost == domain) {
      return true // As in 'example.com' matching 'example.com'.
    }
    return urlHost.endsWith(domain) &&
      urlHost[urlHost.length - domain.length - 1] == '.' &&
      !canParseAsIpAddress(urlHost)
  }

  private static pathMatch(url: HttpUrl, path: string): boolean {
    let urlPath = url.encodedPath
    if (urlPath == path) {
      return true // As in '/foo' matching '/foo'.
    }
    if (urlPath.startsWith(path)) {
      if (path.endsWith("/")) return true // As in '/' matching '/foo'.
      if (urlPath[path.length] == '/') return true // As in '/foo' matching '/foo/bar'.
    }
    return false
  }


    /**
   * Attempt to parse a `Set-Cookie` HTTP header value [setCookie] as a cookie. Returns null if
   * [setCookie] is not a well-formed cookie.
   */
  // @JvmStatic
  static parse(url: HttpUrl, setCookie: string): Cookie | null {
    return this.#parse(Date.now(), url, setCookie)
  }

  static #parse(currentTimeMillis: number, url: HttpUrl, setCookie: string): Cookie | null {
    let cookiePairEnd = delimiterOffset(setCookie,';')
    let pairEqualsSign = delimiterOffset(setCookie,'=',0,cookiePairEnd)
    if (pairEqualsSign == cookiePairEnd) return null

    let cookieName = trimSubstring(setCookie,0,pairEqualsSign)
    if (cookieName === '' || indexOfControlOrNonAscii(cookieName) != -1) return null

    let cookieValue = trimSubstring(setCookie,pairEqualsSign + 1, cookiePairEnd)
    if (indexOfControlOrNonAscii(cookieValue) != -1) return null

    let expiresAt = MAX_DATE
    let deltaSeconds = -1
    let domain: string | null = null
    let path: string | null = null
    let secureOnly = false
    let httpOnly = false
    let hostOnly = true
    let persistent = false

    let pos = cookiePairEnd + 1
    let limit = setCookie.length
    while (pos < limit) {
      let attributePairEnd = delimiterOffset(setCookie,';', pos, limit)

      let attributeEqualsSign = delimiterOffset(setCookie,'=', pos, attributePairEnd)
      let attributeName = trimSubstring(setCookie,pos, attributeEqualsSign)
      let attributeValue = ''
      if (attributeEqualsSign < attributePairEnd) {
        attributeValue = trimSubstring(setCookie,attributeEqualsSign + 1, attributePairEnd)
      }
      if (attributeName.toLowerCase() === 'expires'){
        try {
          expiresAt = this.parseExpires(attributeValue, 0, attributeValue.length)
          persistent = true
        } catch (_) {
          // Ignore this attribute, it isn't recognizable as a date.
        }
      } else if (attributeName.toLowerCase() === 'max-age'){
        try {
          deltaSeconds = this.parseMaxAge(attributeValue)
          persistent = true
        } catch (_) {
          // Ignore this attribute, it isn't recognizable as a max age.
        }
      } else if (attributeName.toLowerCase() === 'domain'){
        try {
          domain = this.parseDomain(attributeValue)
          hostOnly = false
        } catch (_) {
          // Ignore this attribute, it isn't recognizable as a domain.
        }
      } else if (attributeName.toLowerCase() === 'path'){
        path = attributeValue
      } else if (attributeName.toLowerCase() === 'secure'){
        secureOnly = true
      } else if (attributeName.toLowerCase() === 'httponly'){
        httpOnly = true
      }
    pos = attributePairEnd + 1
    }

    // If 'Max-Age' is present, it takes precedence over 'Expires', regardless of the order the two
    // attributes are declared in the cookie string.
    if (deltaSeconds == Number.MIN_VALUE) {
      expiresAt = Number.MIN_VALUE
    } else if (deltaSeconds != -1) {
      let deltaMilliseconds: number
        if (deltaSeconds <= Number.MAX_VALUE / 1000) {
        deltaMilliseconds = deltaSeconds * 1000
      } else {
        deltaMilliseconds = Number.MAX_VALUE
      }
      expiresAt = currentTimeMillis + deltaMilliseconds
      if (expiresAt < currentTimeMillis || expiresAt > MAX_DATE) {
        expiresAt = MAX_DATE // Handle overflow & limit the date range.
      }
    }

    // If the domain is present, it must domain match. Otherwise we have a host-only cookie.
    let urlHost = url.host
    if (domain === null || domain === undefined) {
      domain = urlHost
    } else if (!this.domainMatch(urlHost, domain)) {
      return null // No domain match? This is either incompetence or malice!
    }

    // If the domain is a suffix of the url host, it must not be a public suffix.
    // if (urlHost.length != domain.length && PublicSuffixDatabase.get().getEffectiveTldPlusOne(domain) == null) {
    //    // 判断是否为公共后缀:考虑使用js库psl替代
    //   return null
    // }

    // If the path is absent or didn't start with '/', use the default path. It's a string like
    // '/foo/bar' for a URL like 'http://example.com/foo/bar/baz'. It always starts with '/'.
    if (path === null || path === undefined || !path.startsWith("/")) {
      let encodedPath = url.encodedPath
      let lastSlash = encodedPath.lastIndexOf('/')
      if (lastSlash != 0) path = encodedPath.substring(0, lastSlash)
      else path = "/"
    }

    return new Cookie(cookieName, cookieValue, expiresAt, domain, path, secureOnly, httpOnly,
      persistent, hostOnly)
  }

  /** Parse a date as specified in RFC 6265, section 5.1.1. */
  private static parseExpires(s: string, pos: number, limit: number): number {
    var pos = pos
    pos = this.dateCharacterOffset(s, pos, limit, false)

    var hour = -1
    var minute = -1
    var second = -1
    var dayOfMonth = -1
    var month = -1
    var year = -1
    while (pos < limit) {
      let end = this.dateCharacterOffset(s, pos + 1, limit, true)
      let token = s.slice(pos, end)
      
      // Match patterns in order, like OkHttp's when expression.
      // Stop after first match to avoid matching the same token as multiple types.
      let matcher1 = token.match(Cookie.TIME_PATTERN)
      if (hour == -1 && matcher1){
        hour = parseInt(matcher1[1],10)
        minute = parseInt(matcher1[2],10)
        second = parseInt(matcher1[3],10)
      } else {
        let matcher2 = token.match(Cookie.DAY_OF_MONTH_PATTERN)
        if (dayOfMonth == -1 && matcher2){
          dayOfMonth = parseInt(matcher2[1],10)
        } else {
          let matcher3 = token.match(Cookie.MONTH_PATTERN)
          if (month == -1 && matcher3){
            let monthString = matcher3[1].toLowerCase()
            const monthMap: Record<string, number> = {
              'jan': 0, 'feb': 1, 'mar': 2, 'apr': 3, 'may': 4, 'jun': 5,
              'jul': 6, 'aug': 7, 'sep': 8, 'oct': 9, 'nov': 10, 'dec': 11
            }
            month = monthMap[monthString]
          } else {
            let matcher4 = token.match(Cookie.YEAR_PATTERN)
            if (year == -1 && matcher4){
              year = parseInt(matcher4[1],10)
            }
          }
        }
      }
      pos = this.dateCharacterOffset(s, end + 1, limit, false)
    }

    // Convert two-digit years into four-digit years. 99 becomes 1999, 15 becomes 2015.
    if (year >= 70 && year <= 99) year += 1900
    if (year >= 0 && year <= 69) year += 2000

    // If any partial is omitted or out of range, return -1. The date is impossible. Note that leap
    // seconds are not supported by this syntax.
    kotlinRequire(year >= 1601,'')
    kotlinRequire(month != -1,'')
    kotlinRequire(dayOfMonth >= 1 && dayOfMonth <= 31, '')
    kotlinRequire(hour >= 0 && hour <= 23,'')
    kotlinRequire(minute >= 0 && minute <= 59,'')
    kotlinRequire(second >= 0 && second <= 59,'')
    const utcTime = Date.UTC(year, month, dayOfMonth, hour, minute, second, 0);
    return utcTime
  }

  /**
   * Returns the index of the next date character in `input`, or if `invert` the index
   * of the next non-date character in `input`.
   */
  private static dateCharacterOffset(input: string, pos: number, limit: number, invert: boolean): number {
    for (let i = pos; i < limit; i++) {
      let c = input.charCodeAt(i)
      let dateCharacter = (c < ' '.charCodeAt(0) && c !== '\t'.charCodeAt(0)) || // 控制字符但不是 \t
        c >= '\u007f'.charCodeAt(0) ||                         // 非 ASCII 字符
        (c >= '0'.charCodeAt(0) && c <= '9'.charCodeAt(0)) || // 0-9
        (c >= 'a'.charCodeAt(0) && c <= 'z'.charCodeAt(0)) || // a-z
        (c >= 'A'.charCodeAt(0) && c <= 'Z'.charCodeAt(0)) || // A-Z
        c === ':'.charCodeAt(0);                              // :
      if (dateCharacter == !invert) return i
    }
    return limit
  }


  /**
   * Returns the positive value if [s] is positive, or [Long.MIN_VALUE] if it is either 0 or
   * negative. If the value is positive but out of range, this returns [Long.MAX_VALUE].
   *
   * @throws NumberFormatException if [s] is not an integer of any precision.
   */
  private static parseMaxAge(s: string): number {
    // 去除空白
    s = s.trim();

    // 正则：匹配可选负号 + 一个或多个数字
    const integerPattern = /^-?\d+$/;

    if (!integerPattern.test(s)) {
      throw new IllegalArgumentException(`Invalid integer: ${s}`);
    }

    // 尝试用 BigInt 解析（BigInt 能处理任意精度整数）
    let parsed: number;
    try {
      parsed = parseInt(s,10);
    } catch {
      throw new IllegalArgumentException(`Invalid integer: ${s}`);
    }

    // 判断是否在 Long 范围内：[LONG_MIN_VALUE, LONG_MAX_VALUE]
    if (parsed > Number.MAX_VALUE) {
      return Number.MAX_VALUE; // 正数太大
    }

    if (parsed < Number.MIN_VALUE) {
      return Number.MIN_VALUE; // 负数太小（理论上 BigInt 不会溢出，但语义上处理）
    }

    // 现在 parsed 在 Long 范围内
    if (parsed <= 0) {
      return Number.MIN_VALUE;
    } else {
      return parsed;
    }
  }


  /**
   * Returns a domain string like `example.com` for an input domain like `EXAMPLE.COM`
   * or `.example.com`.
   */
  private static parseDomain(s: string): string {
    kotlinRequire(!s.endsWith("."),'domain is not endwith .')
    let result: string | null
    result = toCanonicalHost(kotlinRemovePrefix(s,'.'))
    if (result === null) {
      throw new IllegalArgumentException(`unexpected domain: ${s}`)
    }
    return result
  }


  /** Returns all of the cookies from a set of HTTP response headers. */
  // @JvmStatic
  static parseAll(url: HttpUrl, headers: Headers): List<Cookie> {
    let cookieStrings = headers.values("Set-Cookie") || ''
    let cookies: List<Cookie> | null = null

    for (let i = 0; i < cookieStrings.length; i++) {
      let cookie = this.parse(url, cookieStrings[i])
      if (cookie === null || cookie === undefined){
        continue
      }
      if (cookies === null || cookies === undefined) cookies = new List<Cookie>()
      cookies.add(cookie)
    }

    if (cookies !== null && cookies !== undefined){
      return cookies
    }
    return new List<Cookie>()
  }
}